3장. 글 말고 그림과 표까지 찾기
출처: 『RAG 마스터: 랭체인으로 완성하는 LLM 서비스』(브라이스 유·조경아·박수진·김재웅 지음, 프리렉 2025) | 공식: www.langchain.com
코드는 분위기만 —
partition_pdf·base64·MultiVectorRetriever같은 말은 몰라도 됩니다. 표의 '비유'와 '위험'만 봐도 충분해요.
지금까지 다룬 RAG는 글만 찾았다.
그런데 세상 문서에는 글만 있지 않다.
사진, 차트, 표가 같이 들어 있다.
이 장은 그림과 표까지 함께 찾아 답하는 법을 본다.
0. 이 장의 새 단어
0장 용어집에 없는 말 3개만 미리 풀어 둔다.
본문에서 막히면 여기로 돌아오면 된다.
(0장에 있는 멀티모달·임베딩·벡터·검색기·청크는 그대로 쓴다.)
모달리티(modality)
한 문장 뜻 — 데이터의 형태·종류. 글, 사진, 소리, 표, 차트가 각각 한 가지 모달리티다.
일상비유 — 감각의 종류. 눈으로 보는 것, 귀로 듣는 것, 손으로 만지는 것은 서로 다른 통로다. 데이터도 글·그림·소리라는 다른 통로가 있다.
한 줄 예 —
# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# 글은 한 모달리티, 사진은 또 다른 모달리티
text = "사과는 빨갛다" # 글 모달리티
image = "apple.jpg" # 사진 모달리티
멀티모달 LLM(MLLM, multimodal LLM)
한 문장 뜻 — 글만이 아니라 사진까지 입력으로 받아 알아보고 답하는 AI 모델.
일상비유 — 글도 읽고 그림도 보는 친구. 보통 친구는 글로 적어 줘야 알지만, 이 친구는 사진을 그냥 보여 줘도 "이건 강아지네" 하고 말해 준다.
한 줄 예 —
# 사진을 보여 주면 글로 설명해 주는 AI
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
mllm.invoke("이 사진에 뭐가 있나요?", image="dog.jpg")
# → "갈색 강아지가 풀밭에 앉아 있습니다"
CLIP(클립)
한 문장 뜻 — 글과 사진을 같은 숫자 공간에 나란히 놓는, 오픈AI가 만든 모델.
일상비유 — 글과 그림을 같은 자로 재는 도구. 보통은 글 자와 그림 자가 따로라 둘을 못 비교한다. CLIP은 한 자로 둘 다 재서, "이 글과 이 사진이 비슷하다"를 바로 말해 준다.
한 줄 예 —
# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# 글도 사진도 같은 숫자 묶음으로 바꿔 비교
text_vec = clip.encode("강아지 사진") # 글 → 숫자
img_vec = clip.encode("dog.jpg") # 사진 → 숫자
# 두 숫자가 비슷하면 글과 사진이 어울린다
이 장에서 딱 4가지만
RAG에 사진·표·차트까지 보태면, 답이 더 정확해진다. 그게 멀티모달 RAG다.
글과 사진을 한 창고에 넣는 길은 세 갈래다. 같이 숫자로 바꾸기 / 사진을 글로 바꾸기 / 따로따로 두 창고 쓰기.
사진은 글이 아니라서 그냥 못 보낸다. base64라는 텍스트 옷으로 갈아입혀 보낸다.
요약본으로 찾고 원본을 돌려주는 똑똑한 검색기(MultiVectorRetriever)를 쓰면, 검색은 깔끔하고 답은 자세하다.
1. 왜 글만으론 모자랄까
망가지는 장면
온라인 쇼핑몰에서 신발을 찾는다.
"검정 운동화"라고 글로만 검색했다.
그런데 내가 원한 건 그 사진 속 디자인이었다.
글만으론 디자인을 다 말로 못 적는다.
결국 엉뚱한 신발만 잔뜩 나온다.
회사 보고서도 마찬가지다.
핵심 숫자는 차트 그림 안에 있는데, 글만 검색하면 그 차트를 못 찾는다.
비유
요리 레시피를 글로만 읽는 것과 같다.
"반죽을 적당히 치댄다"는 글만으론 감이 안 온다.
옆에 반죽 사진이 있으면 단번에 이해된다.
글에 사진을 보태면 정보가 훨씬 풍부해진다.
비유↔코드↔위험
| 비유 | 코드 | 위험 |
|---|---|---|
| 글로만 신발 검색 | search("검정 운동화") |
디자인은 사진에 있어 못 찾음 |
| 사진까지 같이 검색 | search("검정 운동화", image="shoe.jpg") |
사진 다룰 모델이 없으면 불가 |
정의
멀티모달 RAG는 글뿐 아니라 사진·차트·표 같은 시각·구조 데이터까지 함께 찾아 답을 만드는 RAG다.
모달리티는 데이터의 형태(글·사진·소리·표)를 뜻한다.
멀티모달은 그 여러 형태를 동시에 쓰는 방식이다.
예시 폭격
예시 1 — 완성 예 (왜 더 정확한가)
# 글만 줄 때
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
answer = llm.invoke("이 보고서의 환자 추세는?")
# → "정보가 부족합니다" (추세는 차트 그림에 있음)
# 사진(차트)까지 줄 때
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
answer = mllm.invoke("이 보고서의 환자 추세는?", image="chart.jpg")
# → "최근 4주간 환자가 줄었습니다" (차트를 직접 봄)
예시 2 — 부분 완성 (빈칸 채우기)
쇼핑몰에서 "빨간 원피스"를 글과 사진으로 같이 검색한다.
# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
results = search("빨간 원피스", image="_______") # ← 원피스 사진 파일 이름
빈칸엔 찾고 싶은 옷의 사진 경로가 들어간다.
글(빨간 원피스) + 사진(디자인)이 합쳐져 더 정확히 걸린다.
예시 3 — 독립 적용 (직접 판단)
여행 후기 글 + 풍경 사진이 섞인 블로그가 있다.
"바다가 보이는 숙소" 질문에 글만 검색하면?
→ 글에 "바다"라는 단어가 없으면 못 찾는다.
사진까지 검색하면?
→ 풍경 사진 속 바다를 보고 찾아낸다.
미니 시나리오
이럴 때 이렇게 — 제품 매뉴얼이 글 반, 그림 반이다.
"버튼 누르는 위치"를 묻는다.
글에는 "오른쪽 버튼"이라고만 적혀 있어 헷갈린다.
→ 그림까지 검색하는 멀티모달 RAG로 바꾸면, 버튼 위치 그림을 같이 보여 줄 수 있다.
2. 멀티모달 RAG가 왜 어려울까
망가지는 장면
사진이라고 다 같은 사진이 아니다.
풍경 사진은 "바다, 나무, 모래" 정도만 알면 된다.
그런데 보고서 차트는 숫자 하나하나가 중요하다.
둘을 똑같이 처리하면, 차트의 숫자를 다 놓친다.
비유
사진을 보는 두 가지 눈이 있다.
여행 사진은 대충 본다 — 분위기만 알면 된다.
영수증 사진은 꼼꼼히 본다 — 금액 한 자리도 틀리면 안 된다.
사진마다 봐야 하는 깊이가 다르다.
비유↔코드↔위험
| 비유 | 코드 | 위험 |
|---|---|---|
| 풍경은 대충 보기 | summarize("연못, 나무, 모래") |
차트를 대충 보면 숫자 다 놓침 |
| 차트는 꼼꼼히 보기 | summarize("42주 665명, 전년比 감소") |
맥락 빠뜨리면 숫자 뜻 모름 |
정의
멀티모달 RAG가 어려운 이유는 두 가지다.
하나, 유형별로 요구가 다르다 — 풍경은 개요만, 차트는 세부 숫자와 맥락까지.
둘, 데이터 일관성 — 같은 문서 안 글과 차트가 서로 어긋나면 안 된다.
예시 폭격
예시 1 — 완성 예 (일관성이 깨지는 경우)
# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
text = "환자가 늘었습니다" # 글은 '증가'라고 말함
chart_summary = "환자가 줄었습니다" # 차트는 '감소'를 보여줌
# 둘이 어긋나면 → AI가 헷갈려 틀린 답을 만든다
예시 2 — 부분 완성 (어느 쪽을 꼼꼼히 볼까)
# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
photo = "여름휴가 바다 사진" # 처리 방식: ________ (대충 / 꼼꼼히)
report_chart = "매출 막대그래프" # 처리 방식: ________ (대충 / 꼼꼼히)
여행 사진은 대충, 매출 차트는 꼼꼼히가 답이다.
예시 3 — 독립 적용
회사 발표 슬라이드 한 장에 제목 글과 매출 차트가 같이 있다.
이 한 장을 멀티모달 RAG에 넣으려면 무엇을 점검할까?
→ 글과 차트가 같은 말을 하는지(일관성), 차트 숫자를 꼼꼼히 읽었는지(요구 수준) 둘 다 본다.
미니 시나리오
이럴 때 이렇게 — PDF 한 권에 풍경 사진과 통계 차트가 섞여 있다.
→ 풍경은 짧게 요약하고, 차트는 숫자까지 자세히 요약하도록 따로 다룬다.
3. 글과 그림을 한 창고에 넣는 세 가지 길
망가지는 장면
글은 글 창고, 사진은 사진 창고에 따로 두면?
질문 하나에 두 창고를 다 뒤져야 한다.
게다가 어느 창고 답이 더 맞는지도 모른다.
서로 다른 모달리티를 어떻게 한데 모아 검색할지가 핵심 고민이다.
비유
손님 짐을 맡는 보관소를 떠올리자.
방법은 세 가지다.
하나, 모든 짐을 같은 규격 상자에 넣어 한 칸에 (동일 공간).
둘, 짐을 다 사진 찍어 글로 적어 장부 한 권에 (글로 통일).
셋, 두 장부를 따로 두고 나중에 골라 쓰기 (별도 저장소).
비유↔코드↔위험
| 비유 | 코드 | 위험 |
|---|---|---|
| 같은 상자에 다 담기 | clip.encode(글_또는_사진) |
글·사진 다 잘 담는 모델이 있어야 함 |
| 사진을 글로 적어 두기 | summary = mllm.invoke(사진) |
적는 과정에서 사진 디테일 손실 |
| 두 장부 따로 두기 | store1.search() + store2.search() |
손이 두 배로 가고 비용 ↑ |
정의
세 가지 길이 있다.
길 A(동일 벡터 공간) — CLIP 같은 모델로 글·사진을 같은 숫자 공간에 넣는다.
길 B(기본 모달리티 통일) — 사진을 글 요약으로 바꿔 글만 다룬다.
길 C(별도 저장소) — A와 B를 둘 다 써서 두 창고를 운영하고 골라 쓴다.
두 창고 결과를 한 번 더 줄 세워 제일 좋은 것만 고르는데, 이 줄 세우기를 리랭킹이라 부른다.
길 A·B·C 한눈에 비교
| 길 | 한마디 | 좋은 점 | 나쁜 점 |
|---|---|---|---|
| A 동일 공간 | 글·사진을 같은 숫자로 | 기존 RAG 그대로, 만들기 쉬움 | 강한 멀티모달 모델 꼭 필요 |
| B 글로 통일 | 사진을 글 요약으로 | 따로 사진 모델 불필요, 실용적 | 요약 비용·사진 디테일 손실 |
| C 두 창고 | A+B 합치고 골라 쓰기 | 최고 품질 답 | 시간 두 배, 비용 두 배 |
예시 폭격
예시 1 — 완성 예 (길 A: CLIP으로 같은 공간에)
# CLIP은 글도 사진도 같은 숫자 공간에 넣는다
# 문장이나 문서를 의미 벡터로 바꾸는 임베딩 단계를 준비합니다.
clip = OpenCLIPEmbeddings(model_name="ViT-B-32", checkpoint="openai")
# 문장이나 문서를 의미 벡터로 바꾸는 임베딩 단계를 준비합니다.
store = Chroma(embedding_function=clip)
store.add_texts(["빨간 사과"]) # 글 추가
store.add_images(uris=["apple.jpg"]) # 사진 추가 (같은 공간!)
# 글 질문 하나로 글·사진을 동시에 찾음
# 문서 벡터를 저장하고 비슷한 내용을 찾을 저장소를 준비합니다.
results = store.similarity_search("빨간 과일")
예시 2 — 부분 완성 (길 B: 사진을 글로)
# 준비한 흐름에 실제 입력을 넣어 결과를 확인합니다.
summary = mllm.invoke("이 사진을 검색용으로 짧게 요약해줘", image="chart.jpg")
# → "________" ← 사진을 설명하는 글 한 줄이 채워짐
# 이제 이 글을 보통 RAG처럼 다루면 끝
예시 3 — 독립 적용 (어느 길을 고를까)
빠르게 시제품만 만들고 싶다 → 길 ___ (사진을 글로 바꾸는 게 제일 간단).
사진 자체로 직접 비교 검색이 꼭 필요하다 → 길 ___ (CLIP으로 같은 공간).
품질이 최우선이고 비용은 괜찮다 → 길 ___ (두 창고 다 운영).
답: B / A / C.
미니 시나리오
이럴 때 이렇게 — 사내 위키에 글과 도표가 섞여 있고, 빨리 검색을 붙이고 싶다.
→ 길 B를 고른다. 도표를 글 요약으로 바꿔 기존 글 RAG에 그대로 얹는다.
4. CLIP — 글과 사진을 같은 자로 재기
망가지는 장면
"강아지"라는 글과 강아지 사진이 있다.
컴퓨터는 글 숫자와 사진 숫자가 서로 다른 자로 매겨져 둘을 못 비교한다.
글 자와 그림 자가 따로면, "이 글과 이 사진이 어울린다"를 영영 못 잰다.
비유
길이는 cm, 무게는 kg으로 잰다.
서로 단위가 달라 "3cm와 5kg 중 뭐가 크냐"는 말이 안 된다.
CLIP은 글과 사진을 같은 단위로 바꿔, 비로소 둘을 견준다.
비유↔코드↔위험
| 비유 | 코드 | 위험 |
|---|---|---|
| 사진 → 같은 자 | clip.encode("dog.jpg") |
사진 인코더가 약하면 엉터리 숫자 |
| 글 → 같은 자 | clip.encode("강아지") |
글 인코더가 약하면 비교 어긋남 |
정의
CLIP은 사진 인코더와 글 인코더 둘로 이뤄진다.
같은 짝(강아지 글 + 강아지 사진)은 가깝게, 다른 짝은 멀게 만들도록 훈련한다.
이걸 대조 학습이라 한다.
덕분에 따로 안 배운 새 사진도 글로 설명할 수 있다(zero-shot).
예시 폭격
예시 1 — 완성 예 (글로 사진 찾기)
# "고양이 사진 찾아줘" — 글로 사진을 검색
# `query_vec`에 중간 결과를 담아 다음 줄에서 재사용합니다.
query_vec = clip.encode("귀여운 고양이")
# 사진 창고에서 이 숫자와 가까운 사진을 꺼냄
# → cat1.jpg, cat2.jpg ...
예시 2 — 부분 완성
# `photo_vec`에 중간 결과를 담아 다음 줄에서 재사용합니다.
photo_vec = clip.encode("apple.jpg")
text_vec = clip.encode("__________") # ← 사진과 어울리는 글
# 빈칸에 "빨간 사과"를 넣으면 두 숫자가 가깝게 나온다
예시 3 — 독립 적용
CLIP에 한 번도 안 보여 준 "분홍 우산" 사진을 준다.
CLIP은 이걸 글로 설명할 수 있을까?
→ 할 수 있다. 따로 안 배웠어도 글·사진을 같은 자로 재 두었기 때문이다(zero-shot).
미니 시나리오
이럴 때 이렇게 — 사진만 잔뜩 있는 앨범에서 "바다 사진"을 글로 찾고 싶다.
→ CLIP으로 사진들을 미리 숫자로 바꿔 두면, "바다"라는 글 숫자와 가까운 사진이 바로 걸린다.
5. 사진을 글 옷으로 — base64
망가지는 장면
사진을 AI에게 보내려 한다.
그런데 사진은 글이 아니다.
채팅 칸에 사진 파일을 그냥 못 붙인다.
AI API는 글(텍스트)만 받는데, 사진은 0과 1 덩어리라 그대로는 안 들어간다.
비유
해외로 짐을 부칠 때 규격 상자에 다시 담는 것과 같다.
물건 모양이 제각각이면 못 부친다.
정해진 상자(텍스트)에 옮겨 담아야 보낼 수 있다.
base64는 사진을 글자만으로 된 상자에 옮겨 담는 일이다.
비유↔코드↔위험
| 비유 | 코드 | 위험 |
|---|---|---|
| 사진을 글자 상자에 | base64.b64encode(사진).decode() |
안 바꾸면 API가 못 받음 |
| 상자째 AI에 보내기 | f"data:image/jpeg;base64,{글자}" |
형식 틀리면 인식 실패 |
정의
base64는 사진 같은 이진 데이터를 글자(ASCII)로 바꾸는 방식이다.
쓰는 글자는 64개다 — 대문자 A~Z, 소문자 a~z, 숫자 0~9, 그리고 +, /.
이렇게 글자로 바꾸면 전송 중 손상 없이 안전하게 보낼 수 있다.
예시 폭격
예시 1 — 완성 예 (사진을 글자로)
# 필요한 라이브러리나 클래스를 가져와서 아래 예제에서 쓸 준비를 합니다.
import base64
# 반복해서 쓸 처리 흐름에 이름을 붙인 함수입니다.
def encode_image(path):
with open(path, "rb") as f: # 사진을 통째로 읽고
return base64.b64encode(f.read()).decode("utf-8") # 글자로 바꿈
# `b64`에 중간 결과를 담아 다음 줄에서 재사용합니다.
b64 = encode_image("chart.jpg")
# b64 → "iVBORw0KGgoAAAA..." (글자만 잔뜩)
예시 2 — 부분 완성 (AI에 보낼 형식)
# `message`에 중간 결과를 담아 다음 줄에서 재사용합니다.
message = {
# 이 줄은 예제 흐름을 이루는 실제 처리 단계입니다.
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{________}"}, # ← 위에서 만든 b64
}
빈칸엔 글자로 바꾼 사진(b64)이 들어간다.
예시 3 — 독립 적용
PDF에서 막 뽑아낸 차트 그림 파일이 있다.
이걸 gpt-4o에게 "무슨 차트인지 설명해줘"라고 보내려면 먼저 뭘 할까?
→ base64로 글자 옷을 입혀야 한다. 그래야 API가 받는다.
미니 시나리오
이럴 때 이렇게 — 인터넷에 올라간 사진은 주소(URL)가 있다.
그땐 URL을 그냥 보내면 된다.
하지만 내 컴퓨터에서 막 뽑은 사진은 주소가 없다.
→ base64로 글자 옷을 입혀 직접 보낸다.
6. 똑똑한 검색기 — 요약으로 찾고 원본을 돌려준다
망가지는 장면
차트 표를 글자 그대로 창고에 넣었다.
"2023년 2022년 2024년 42주 747 294 420..." 이렇게 뒤죽박죽이다.
이 지저분한 글로 검색하니 잘 안 걸린다.
그렇다고 깔끔한 요약만 저장하면, 정작 답할 땐 자세한 원본이 없어 곤란하다.
비유
도서관 카드 목록과 같다.
서가에 책을 통째로 늘어놓고 찾으면 너무 번잡하다.
대신 얇은 요약 카드로 찾고, 카드가 가리키는 진짜 책을 꺼내 준다.
찾기는 카드로 가볍게, 읽기는 원본으로 자세하게.
비유↔코드↔위험
| 비유 | 코드 | 위험 |
|---|---|---|
| 얇은 카드로 찾기 | vectorstore (요약 저장) |
요약이 엉성하면 못 찾음 |
| 진짜 책 꺼내 주기 | docstore (원본 저장) |
원본 안 두면 답이 빈약 |
정의
MultiVectorRetriever는 창고를 둘로 나눈다.
vectorstore에는 간결한 요약을 넣어 검색에 쓴다.
docstore에는 자세한 원본(글·표·사진)을 넣어 답에 쓴다.
요약으로 찾아 그 요약이 가리키는 원본을 돌려준다.
예시 폭격
예시 1 — 완성 예 (두 창고 나누기)
vectorstore = Chroma(embedding_function=OpenAIEmbeddings()) # 요약용
docstore = InMemoryStore() # 원본용
# 문서 벡터를 저장하고 비슷한 내용을 찾을 저장소를 준비합니다.
retriever = MultiVectorRetriever(
vectorstore=vectorstore, # 여기서 찾고
docstore=docstore, # 여기서 원본 꺼냄
# `id_key`에 중간 결과를 담아 다음 줄에서 재사용합니다.
id_key="doc_id",
)
예시 2 — 부분 완성 (요약과 원본 짝짓기)
# 아래 예제는 핵심 흐름만 짧게 보여 줍니다.
# 원본 글은 docstore에, 그 요약은 vectorstore에 — 같은 id로 묶음
doc_ids = [str(uuid.uuid4()) for _ in texts] # 짝지을 번호표
retriever.docstore.mset(list(zip(doc_ids, ________))) # ← 원본 글(texts)
빈칸엔 원본 글 목록(texts)이 들어간다.
번호표(doc_id)로 요약과 원본을 이어 둔다.
예시 3 — 독립 적용
"말라리아 군집 사례"를 검색했다.
검색기가 요약으로 찾은 뒤 무엇을 돌려줄까?
→ 요약이 아니라, 그 요약이 가리키는 원본 표와 사진을 돌려준다.
그래야 자세한 답을 만들 수 있다.
미니 시나리오
이럴 때 이렇게 — 긴 보고서 한 단락이 검색에 안 걸린다.
→ 그 단락을 한 줄로 요약해 vectorstore에 넣고, 원본은 docstore에 둔다.
짧은 요약이라 검색은 잘 걸리고, 답할 땐 원본 단락을 그대로 쓴다.
7. PDF에서 글·표·사진 한꺼번에 뽑기
망가지는 장면
PDF 안에는 글·표·사진이 뒤엉켜 있다.
손으로 하나씩 떼어내려면 끝이 없다.
게다가 표는 칸 구조가 깨지고, 사진은 따로 저장도 안 된다.
비유
믹서기에 통째로 넣고 버튼 하나 누르는 것과 같다.
과일·채소를 따로 손질할 필요 없다.
도구가 알아서 갈아 종류별로 담아 준다.
unstructured가 PDF를 그렇게 글·표·사진으로 갈라 준다.
비유↔코드↔위험
| 비유 | 코드 | 위험 |
|---|---|---|
| 한 번에 갈기 | partition_pdf(filename=...) |
tesseract·poppler 먼저 깔아야 함 |
| 종류별로 담기 | 타입 보고 tables / texts 분류 |
설치 순서 틀리면 에러 |
정의
unstructured는 PDF에서 글·표·사진을 자동으로 뽑아 주는 도구다.
핵심 함수는 partition_pdf다.
이 도구는 tesseract(글자 읽기)와 poppler(PDF 다루기)에 기댄다.
그래서 이 둘을 먼저 깔고 unstructured를 깔아야 한다.
예시 폭격
예시 1 — 완성 예 (PDF 한 방에 뽑기)
# `raw`에 중간 결과를 담아 다음 줄에서 재사용합니다.
raw = partition_pdf(
# `filename`에 중간 결과를 담아 다음 줄에서 재사용합니다.
filename="sample.pdf",
extract_images_in_pdf=True, # 사진도 뽑기
infer_table_structure=True, # 표 구조도 살리기
chunking_strategy="by_title", # 제목 단위로 자르기
)
예시 2 — 부분 완성 (종류별로 나누기)
# `tables, texts`에 중간 결과를 담아 다음 줄에서 재사용합니다.
tables, texts = [], []
# 목록이나 결과 묶음을 하나씩 꺼내 같은 처리를 반복합니다.
for el in raw:
# `t`에 중간 결과를 담아 다음 줄에서 재사용합니다.
t = str(type(el))
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
if "Table" in t:
tables.append(str(el)) # 표
elif "________" in t: # ← CompositeElement (글 덩어리)
texts.append(str(el)) # 글
빈칸엔 글을 뜻하는 CompositeElement가 들어간다.
예시 3 — 독립 적용
WSL(우분투)에서 이 코드를 처음 돌린다.
unstructured를 바로 pip install 했더니 에러가 난다.
왜일까?
→ tesseract와 poppler를 먼저 안 깔았기 때문이다.
순서를 지켜 둘을 먼저 깔면 해결된다.
미니 시나리오
이럴 때 이렇게 — 차트가 든 PDF 보고서를 RAG에 넣고 싶다.
→ partition_pdf로 글·표·사진을 한 번에 뽑고, 종류별로 따로 담는다.
그다음 표·사진은 글 요약으로 바꿔(길 B) 검색에 쓴다.
8. 다 합쳐 멀티모달 RAG 완성
망가지는 장면
검색기가 결과를 돌려줬다.
그런데 글과 사진이 한데 섞여 한 봉지로 왔다.
어느 게 글이고 어느 게 사진인지 안 갈라 두면, AI에게 제대로 못 넘긴다.
비유
택배 한 상자에 옷과 책이 같이 왔다.
옷은 옷장에, 책은 책장에 나눠 둬야 쓴다.
검색 결과도 글은 글대로, 사진은 사진대로 갈라 둔다.
비유↔코드↔위험
| 비유 | 코드 | 위험 |
|---|---|---|
| 글·사진 갈라 담기 | split_image_text_types(docs) |
안 가르면 AI가 헷갈림 |
| 둘 다 메시지로 묶기 | HumanMessage(content=[글, 사진]) |
사진 형식 틀리면 무시됨 |
정의
검색 결과를 먼저 글과 사진으로 가른다.
base64로 풀리면 사진, 안 풀리면 글로 본다.
그다음 글과 사진을 한 메시지로 묶어 멀티모달 LLM에게 보낸다.
LLM이 글과 사진을 같이 보고 답한다.
예시 폭격
예시 1 — 완성 예 (글·사진 가르기)
# 반복해서 쓸 처리 흐름에 이름을 붙인 함수입니다.
def split_image_text_types(docs):
# `images, texts`에 중간 결과를 담아 다음 줄에서 재사용합니다.
images, texts = [], []
# 목록이나 결과 묶음을 하나씩 꺼내 같은 처리를 반복합니다.
for doc in docs:
# 실패할 수 있는 처리를 먼저 시도합니다.
try:
b64decode(doc) # 풀리면 사진
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
images.append(doc)
# 실패가 발생했을 때 사용자에게 보일 대체 처리를 합니다.
except Exception:
texts.append(doc) # 안 풀리면 글
# 계산하거나 처리한 결과를 이 함수를 부른 쪽으로 돌려줍니다.
return {"images": images, "texts": texts}
예시 2 — 부분 완성 (질문 + 검색결과 묶기)
# 여러 값을 이름표가 붙은 구조로 묶어 전달합니다.
content = [{"type": "text", "text": "다음 문맥으로 답하세요..."}]
# 조건을 확인해서 상황에 맞는 처리 경로를 고릅니다.
if data["context"]["images"]:
# 준비한 함수나 객체를 호출해 예제의 핵심 동작을 실행합니다.
content.append({
"type": "________", # ← image_url (사진 칸)
# 여러 값을 이름표가 붙은 구조로 묶어 전달합니다.
"image_url": {"url": f"data:image/jpeg;base64,{사진}"},
})
빈칸엔 사진을 뜻하는 image_url이 들어간다.
예시 3 — 독립 적용
"말라리아 군집 사례는 어떤가요?"라고 물었다.
검색기가 사진 3개, 글 1개를 돌려줬다.
이제 답을 만들려면?
→ 글과 사진을 한 메시지로 묶어 gpt-4o에게 보낸다.
gpt-4o가 표 글과 차트 사진을 같이 보고 "4개 시도 46건..."이라 답한다.
미니 시나리오
이럴 때 이렇게 — 사용자가 차트가 든 보고서에 질문한다.
→ 요약으로 검색 → 원본(글+사진) 꺼내기 → 글·사진 가르기 → 한 메시지로 묶기 → LLM이 답.
이 한 줄이 멀티모달 RAG 전체 흐름이다.
단순 규칙
글만 있으면 → 기본 RAG로 충분하다.
사진·표·차트가 섞여 있으면 → 멀티모달 RAG다.
처음엔 길 B(사진을 글 요약으로)가 가장 쉽고 실용적이다.
사진을 보낼 땐 base64로 글자 옷을 입힌다.
검색은 요약으로 가볍게, 답은 원본으로 자세하게(MultiVectorRetriever).
unstructured 쓰기 전엔 tesseract·poppler를 먼저 깐다.
한 걸음 더 ▸ (지금 몰라도 됨)
길 C(두 창고)는 가장 좋은 답을 주지만 손이 많이 간다. 지금은 길 B 하나만 손에 익혀도 충분하다.
사진을 AI에 보낼 때 화질을 고를 수 있다. 대충 보기(low)는 토큰을 아끼고, 자세히 보기(high)는 토큰을 많이 쓴다. 차트처럼 세부가 중요할 때만 자세히 보기를 쓰면 된다. 지금은 "화질을 고를 수 있다" 정도만 알면 충분하다.
파이프라인 중간에 내 함수를 끼우거나(람다), 입력을 그대로 흘려보내는(통과) 작은 부품들도 있다. 0장에서 본 체인(컨베이어 벨트)에 딸린 톱니라고만 알아 두면 된다. 지금 코드를 외울 필요는 없다.
CLIP 말고도 글·사진을 같이 다루는 모델은 계속 새로 나온다. 모델 이름·가격은 수시로 바뀌니, 실제로 쓸 땐 공식 목록을 확인하면 된다.
더 해보기
- 책 공식 실습 노트북(코랩 바로 열기) — Ch03. MultiModal RAG (PDF 추출 + 검색기 + 멀티모달 LLM 종합 실습)
- 랭체인 멀티모달 입력 가이드 — how_to/multimodal_inputs (사진을 메시지에 넣는 법)
- MultiVectorRetriever 공식 문서 — how_to/multi_vector (요약으로 찾고 원본 돌려주기)
- CLIP 원본 저장소 — github.com/openai/CLIP (글·사진을 같은 공간에)
- unstructured 공식 문서 — docs.unstructured.io (비정형 문서 뽑기)
모델·가격은 수시로 바뀐다. 책의
gpt-4o표기는 2025년 기준 예시이니, 실제로 쓸 땐 공식 목록을 확인하라: OpenAI. (검증 2026-05-21)
연습문제
- 설명.
글말고 그림과 표까지 찾기의 핵심을 처음 듣는 사람에게 한 문장으로 설명하라. - 구분. 두 개념(
텍스트 RAG,멀티모달 RAG)을 실제 예시 하나로 구분하라. - 적용. 내 프로젝트나 학습 노트에서 이 장의 개념을 적용해 작게 개선할 지점을 하나 고르라.
부록 A. 쉬운 용어 사전
| 용어 | 아주 쉬운 뜻 | 이 장에서 나온 위치 |
|---|---|---|
| 텍스트 RAG | 글 자료를 찾아 붙여 답하는 RAG 방식. | 부록 B와 본문 예시 |
| 멀티모달 RAG | 글뿐 아니라 이미지와 표 같은 여러 자료 형태까지 찾아 붙이는 RAG 방식. | 부록 B와 본문 예시 |
| 요약 검색 | 긴 원본 대신 요약문으로 먼저 관련 자료를 찾는 방식. | 부록 B와 본문 예시 |
| 원본 반환 | 검색된 요약이 아니라 실제 원문 조각을 답변 근거로 돌려주는 방식. | 부록 B와 본문 예시 |
부록 B. 헷갈리는 개념 비교표
| A | B | 구분 포인트 |
|---|---|---|
| 텍스트 RAG | 멀티모달 RAG | 텍스트만 찾는가, 이미지와 표까지 함께 다루는가가 다르다. |
| 요약 검색 | 원본 반환 | 요약은 빨리 찾기 좋고, 원본은 답변 근거로 더 안전하다. |
부록 C. 더 읽을 자료
- 이 장의
더 해보기섹션 — 이미 모아 둔 공식 문서나 실습 링크가 있으면 여기서 먼저 확인한다. - 같은 책의
0장 한눈에 보기— 용어가 막히면 0장의 용어집과 개념 척추로 돌아간다. - 원본 딥다이브판 같은 장 — 입문판을 읽고 큰 흐름이 잡힌 뒤 세부 논리를 더 깊게 확인한다.
- 이 장의
flashcards.json— 읽은 직후 질문만 보고 답을 떠올리는 회상 연습에 쓴다.
부록 D. 연습문제 풀이
- 설명 예시.
글말고 그림과 표까지 찾기는 RAG에서 자료를 더 잘 찾고, 근거를 더 안전하게 붙이고, 답변 흐름을 더 다루기 쉽게 만드는 방법을 보는 장이다. 중요한 것은 용어를 외우는 것이 아니라, 이 개념이 어떤 입력·부품·결정에 영향을 주는지 말로 풀어 보는 것이다. - 구분 예시. 두 개념(
텍스트 RAG,멀티모달 RAG)의 차이는 이렇게 잡으면 된다. 텍스트만 찾는가, 이미지와 표까지 함께 다루는가가 다르다. 실제 사례를 볼 때는 목적, 입력, 실패했을 때의 증상을 따로 적어 보면 헷갈리지 않는다. - 적용 예시. 가장 작은 개선부터 고른다. 예를 들어 이름을 더 분명히 하거나, 평가 기준을 한 줄 추가하거나, 직접 알 필요 없는 내부 정보를 감추는 식으로 시작한다. 한 번에 크게 갈아엎는 것보다 작은 변경 하나를 확인하며 진행하는 쪽이 입문 단계에 맞다.
다음 장 예고
다음 장에서는 검색을 더 똑똑하게 만드는 방법을 본다.
지금은 "글에 사진·표를 보태면 답이 좋아진다"는 한 줄만 들고 가면 충분하다.
클릭하거나 Space를 눌러 뒤집기